from google.appengine.ext import db
from google.appengine.api import users
import logging

log = logging.getLogger('memebase.models')
log.setLevel(logging.DEBUG)


class Root(object):
    @staticmethod
    def clean_all_content():
        from django.conf import settings
        from google.appengine.api import users
        log.warning('Cleaning all content')
        if not settings.DEBUG or not users.is_current_user_admin():
            log.error('%s tried to clean all content', users.get_current_user())
            return
        def delete(model):
            for instance in model.all():
                instance.delete()

        delete(Node)
        delete(Meme)
        delete(Site)

    @staticmethod
    def normalizequery(query):
        """
        Should be used when we return a query.
        This is necessary because google's query are "True" even when they return no results ("empty").
        So we need this to work with patterns like
        if query: do something
        >>> query = Site.all().filter('admin = ', None)
        >>> bool(query)
        True
        >>> bool(Root.normalizequery(query))
        False
        """
        if query.count() > 0:
            return query
        else:
            return []
    @staticmethod
    def normalizeuser(user):
        if user:
            return users.User(user.email().lower())
        else:
            return None



class Site(db.Model):
    """
    >>> user = users.User(email='gmail@gmail.com')
    >>> sitename = 'my site'
    >>> site = Site.create(sitename, user)
    >>> dbsite = Site.get_site_by_name(sitename)
    >>> dbsite.is_admin(user)
    True
    >>> user == dbsite.add_reader(user)
    True
    >>> dbsite.add_reader(user) # return None
    """
    admin = db.UserProperty(required=True)
    authors = db.ListProperty(users.User)
    readers = db.ListProperty(users.User)

    @staticmethod
    def create(name, admin):
        if isinstance(admin, basestring):
            admin = users.User(admin.lower())
        else:
            admin = Root.normalizeuser(admin)
        def transaction(name, admin):
            # ensure uniqueness of site name
            existing = Site.get_site_by_name(name)
            if existing:
                return
            name = Site._normalize_site_name(name)           
            site = Site(key_name=name, admin=admin)
            site.put()
            return site
        return db.run_in_transaction(transaction, name, admin)

    @staticmethod
    def _normalize_site_name(name):
        """
        >>> Site._normalize_site_name(None)
        ''
        >>> Site._normalize_site_name('')
        ''
        >>> Site._normalize_site_name('  a  b cd_f 12 34   ')
        u'a-b-cd_f-12-34'
        """
        from django.template.defaultfilters import slugify
        if name is not None:
            return slugify(name)
        else:
            return ''

    @staticmethod
    def get_site_by_name(name):
        name = Site._normalize_site_name(name)
        if name:
            return Site.get_by_key_name(name)

    def is_admin(self, user):
        user = Root.normalizeuser(user)       
        result = user == self.admin
        log.debug('is_admin(%s), self.admin=%s, result: %s', user, self.admin, result)
        return result

    def is_author(self, user):
        user = Root.normalizeuser(user)
        return self.is_admin(user) or user in self.authors

    def is_reader(self, user):
        user = Root.normalizeuser(user)       
        return user in self.readers

    def _add_user(self, user, users):
        if user in users:
            log.warning('User %s already in users list "%s"', user, users)
            return
        users.append(user)
        return user

    def add_author(self, author):
        return self._add_user(author, self.authors)
    
    def add_reader(self, reader):
        return self._add_user(reader, self.readers)

    @staticmethod
    def get_all_sites():
        return Root.normalizequery(Site.all())

    def contain_meme(self, meme):
        return self == meme.site

    def get_meme_by_name(self, name):
        return Meme.get_meme_by_name(name, site=self)

    def can_view(self, meme, user):
        log.debug('can_view(%s, %s): contain: %s, author: %s, reader: %s, public: %s',
                meme, user,  self.contain_meme(meme), self.is_author(user),
                self.is_reader(user), meme.public_view
                )
        return self.contain_meme(meme) and (self.is_author(user) or self.is_reader(user) or meme.public_view)

    def can_edit(self, meme, user):
        log.debug('can_edit(%s, %s): contain: %s, author: %s, reader: %s, public: %s',
                meme, user,  self.contain_meme(meme), self.is_author(user),
                self.is_reader(user), meme.public_view
                )
        return self.contain_meme(meme) and self.is_author(user)

    def get_all_visible_memes(self, user):
        all_memes = self.meme_set
        if self.is_author(user) or self.is_reader(user):
            return Root.normalizequery(all_memes)
        else:
            return Root.normalizequery(all_memes.filter('public_view = ', True))

    def can_create_meme(self, user):
        return self.is_author(user)

    def sitename(self):
        return str(self.key().name())

    def get_site_prefix(self):
        from django.core.urlresolvers import reverse
        return reverse('sites_root', kwargs={'sitename': self.sitename()})

    def __eq__(self, other):
        myid = self.key()
        otherid = other.key()
        return bool(myid == otherid)

    def __ne__(self, other):
        myid = self.key()
        otherid = other.key()
        return bool(myid != otherid)

    def __repr__(self):
        return '<Site: %s>' % self.sitename()


class Node(db.Model):
    """
    >>> noderoot = Node.create_root()
    >>> noderoot.content
    u'ROOT NODE'
    >>> node1 = noderoot.create_subnode(content='content1')
    >>> node1.full_position()
    '1'
    >>> node1.children()
    []
    >>> node2 = node1.create_node(content='content2')
    >>> node1.full_position()
    '1'
    >>> node2.full_position()
    '2'
    >>> node1_1 = node1.create_subnode(content='content1.1')
    >>> node1_1.full_position()
    '1.1'
    >>> node1_2 = node1.create_subnode(content='content1.2')
    >>> node1_2.full_position()
    '1.2'
    >>> node1_2_1 = node1_2.create_subnode(content='content1.2.1')
    >>> node1_2_1.full_position()
    '1.2.1'
    >>> node1_2_1.full_position(show_root=True)
    'None.1.2.1'
    >>> node1_2_1.parent_meme() is None
    True
    >>> node1_2_2 = node1_2_1.create_node(content='content1.2.2')
    >>> node1_2_2.full_position()
    '1.2.2'
    >>> node1_1_1 = node1_1.create_subnode(content='content1.1.1')
    >>> [ (node.full_position(), level) for node, level in noderoot.transversal() ]
    [('1', xrange(0)), ('1.1', xrange(1)), ('1.1.1', xrange(2)), ('1.2', xrange(1)), ('1.2.1', xrange(2)), ('1.2.2', xrange(2)), ('2', xrange(0))]
    """

    position = db.IntegerProperty() # relative to parent
    root = db.SelfReferenceProperty() # the ROOT NODE
    childrenkeys = db.ListProperty(db.Key) # all children
    content = db.TextProperty()

    @staticmethod
    def create_root():
        # Note: no parent, this instance is really a root in google's database
        node = Node(content='ROOT NODE')
        node.put()
        return node

    def create_subnode(self, content='', position_node=None):
        """Create a node as child of self"""
        def transaction(self, content, position_node):
            children = self.children()
            if position_node:
                position = children.index(position_node) + 1
            else:
                position = len(children) # default: last position
            children_after_position = children[position:]
            # we need to reposition all childen placed after the new node
            for child in children_after_position:
                child.position += 1
                child.put()
            newnodeposition = position + 1
            node = Node(content=content, position=newnodeposition, parent=self.parent() or self, root=self)
            key = node.put()
            self.childrenkeys.insert(position, key)
            self.put()
            return node
        node = db.run_in_transaction(transaction, self, content, position_node)
        return node

    def create_node(self, content=''):
        """Create a node in the same level as self"""
        if self.is_root():
            return None
        return self.root.create_subnode(content, self)

    def save(self, content=None):
        if content is not None:
            self.content = content
        self.put()

    def is_root(self):
        if not self.root:
            if self.parent():
                log.warning('Node %s has not root and parent %s' % (self.name(), self.parent()))
            return True
        else:
            return False

    def children(self):
        if self.childrenkeys:
            return self.get(self.childrenkeys)
        else:
            return []

    def full_position(self, show_root=False):
        parent = self.root
        full_position = [str(self.position)]
        while parent:
            if show_root or not parent.is_root():
                full_position.append(str(parent.position))
            parent = parent.root
        return '.'.join(reversed(full_position))

    def parent_meme(self):
        root = self.parent() or self
        meme = root.meme_set
        if meme.count() != 1:
            log.warning('Node %s with incorrect parent count: %s', root.name(), meme.count())
            return
        return meme[0]

    @staticmethod
    def get_node_by_name(name, meme):
        name = Node._normalize_name(name)
        if name:
            item = Node.get_by_id(name, parent=meme.root_node)
            return item

    @staticmethod
    def _normalize_name(name):
        try:
            return int(name)
        except ValueError:
            return None

    def name(self):
        return Node._normalize_name(self.key().id())

    def transversal(self):
        def walk(root, level=0):
            yield (root, xrange(level))
            for child in root.children():
                for subchild in walk(child, level+1):
                    yield subchild
        for child in self.children():
            for subchild in walk(child):
                yield subchild

    def __cmp__(self, other):
        #FIXME: use position
        myid = int(self.name())
        otherid = int(other.name())
        return myid - otherid

    def __repr__(self):
        return '<Node: %s - parent meme: %s>' % (self.full_position(show_root=True), self.parent_meme())

class Comment(db.Model):
    node = db.ReferenceProperty(Node, required=True)
    content = db.TextProperty(required=True)
    author = db.UserProperty(required=True)

    @staticmethod
    def create(node, content, author):
        meme = node.parent_meme()
        if meme and meme.can_comment(author):
            comment = Comment(node=node, content=content, author=author)
            comment.put()
            return comment


class Meme(db.Model):
    """
    >>> author = users.User('gmail@gmail.com')
    >>> site = Site.create(name='my site', admin=author)
    >>> meme1 = Meme.create('my meme 1', author=author, site=site)
    >>> meme1.tags
    []
    >>> tags = sorted(['cool', 'nice', 'insightful'])
    >>> meme1.tags = tags
    >>> meme1.tags == tags
    True
    >>> meme2 = Meme.create('my meme 2', author=author, site=site)
    >>> meme2.tags = tags + ['meme2tag']
    >>> lkeys = [ meme.key() for meme in MemeTag.memes_by_tag('cool') ]
    >>> meme1.key() in lkeys and meme2.key() in lkeys
    True
    >>> lkeys = [ meme.key() for meme in MemeTag.memes_by_tag('meme2tag') ]
    >>> meme1.key() not in lkeys and meme2.key() in lkeys
    True
    """

    root_node = db.ReferenceProperty(Node, required=True)
    created = db.DateTimeProperty(required=True, auto_now_add=True)   
    public_view = db.BooleanProperty(required=True, default=False)
    author = db.UserProperty(required=False)
    site = db.ReferenceProperty(Site, required=True)
    title = db.StringProperty(required=True)

    @staticmethod
    def create(content, author, site, title='No Title', public_view=False, created=None, tags=None):
        tags = tags or []
        if not site.is_author(author):
            return
        root_node = Node.create_root()
        node = root_node.create_subnode(content=content)
        meme = Meme(root_node=root_node, author=author, site=site, title=title, public_view=public_view)
        meme.put()
        meme.tags = tags
        return meme

    def can_comment(self, user):
        return self.can_view(user)

    def can_view(self, user):
        return self.site.can_view(self, user)

    def can_edit(self, user):
        return self.site.can_edit(self, user)

    def _tags_read(self):
        query_result = self.memetag_set.order('tagname')
        return list(result.tagname for result in query_result)

    def _tags_write(self, tags):
        MemeTag.create_from_tags(meme=self, tags=tags)

    tags = property(_tags_read, _tags_write)
    
    def get_node_by_name(self, name):
        return Node.get_node_by_name(name, self)

    def transversal(self):
        return self.root_node.transversal()

    def nodes(self):
        return self.root_node.children()

    @staticmethod
    def get_meme_by_name(name, site):
        if name is not None:
            name = Meme._normalize_meme_name(name)
            if name:
                meme = Meme.get_by_id(name)
                if meme and site.contain_meme(meme):
                    return meme
        return None

    @staticmethod
    def _normalize_meme_name(name):
        try:
            return int(name)
        except ValueError:
            return None

    def memename(self):
        return str(self.key().id())


    def get_absolute_url(self):
        return self.get_view_url()

    def get_view_url(self):
        from django.core.urlresolvers import reverse
        return reverse('view_meme', kwargs={'sitename': self.site.sitename(), 'memename': self.memename()})

    def get_edit_url(self):
        from django.core.urlresolvers import reverse
        return reverse('edit_meme', kwargs={'sitename': self.site.sitename(), 'memename': self.memename()})

    def __cmp__(self, other):
        myid = int(self.memename())
        otherid = int(other.memename())
        return myid - otherid

    def __repr__(self):
        return '<Meme name: %s - root: %s>' % (self.memename(), self.root_node.name())


class MemeTag(db.Model):
    meme = db.ReferenceProperty(Meme, required=True)
    tagname = db.CategoryProperty(required=False)

    @staticmethod
    def create(meme, tagname):
        tagname = MemeTag._normalize_tagname(tagname)
        memetag = MemeTag(meme=meme, tagname=tagname)
        memetag.put()
        return memetag

    @staticmethod
    def create_from_tags(meme, tags):
        tags = set(MemeTag._normalize_tagname(tag) for tag in tags)
        return [ MemeTag.create(meme=meme, tagname=tagname) for tagname in tags ]

    @staticmethod
    def _normalize_tagname(tagname):
        return str(tagname).lower()

    @staticmethod
    def memes_by_tag(tagname):
        tagname = MemeTag._normalize_tagname(tagname)       
        return (memetag.meme for memetag in MemeTag.all().filter('tagname = ', tagname))
